package com.ziiwee.reduce.rest.jpa;

import com.ziiwee.reduce.rest.condition.Condition;
import com.ziiwee.reduce.rest.condition.ConditionVisitor;
import com.ziiwee.reduce.rest.condition.impls.GeneralCondition;
import com.ziiwee.reduce.rest.condition.impls.LogicCondition;
import com.ziiwee.reduce.rest.condition.impls.LogicOperation;
import org.hibernate.criterion.MatchMode;
import org.springframework.data.jpa.domain.Specification;
import org.springframework.util.CollectionUtils;

import javax.persistence.criteria.CriteriaBuilder;
import javax.persistence.criteria.CriteriaQuery;
import javax.persistence.criteria.Predicate;
import javax.persistence.criteria.Root;
import java.util.ArrayList;
import java.util.List;

public class JpaConditionVisitor<T> implements ConditionVisitor<Specification<T>> {


    @Override
    public Specification<T> visit(GeneralCondition gc) {
        switch (gc.getOperation()) {
            case LIKE:
                return like(gc);
            case IN:
                return in(gc);
            case NOT_IN:
                return notIn(gc);
            case IS_NULL:
                return isNull(gc);
            case IS_NOT_NULL:
                return isNotNull(gc);
            case EQUAL:
                return equal(gc);
            case NOT_EQUAL:
                return notEqual(gc);
            case BETWEEN:
                return between(gc);
            case GREATER_THAN:
                return greaterThan(gc);
            case GREATER_THAN_OR_EQUAL_TO:
                return greaterThanOrEqualTo(gc);
            case LESS_THAN:
                return lessThan(gc);
            case LESS_THAN_OR_EQUAL_TO:
                return lessThanOrEqualTo(gc);
        }
        throw new RuntimeException("无法处理的条件：" + gc);
    }

    @Override
    public Specification<T> visit(LogicCondition lc) {
        List<Specification<T>> specs = new ArrayList<>();
        for (Condition value : lc.getValues()) {
            Specification<T> spec;
            if (value instanceof GeneralCondition) {
                spec = visit((GeneralCondition) value);
            } else {
                spec = visit((LogicCondition) value);
            }
            if (spec != null) {
                specs.add(spec);
            }
        }
        if (CollectionUtils.isEmpty(specs)) {
            return null;
        }
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            List<Predicate> predicates = new ArrayList<>();
            for (Specification<T> spec : specs) {
                predicates.add(spec.toPredicate(root, query, builder));
            }
            if (lc.getOperation().equals(LogicOperation.AND)) {
                return builder.and(predicates.toArray(new Predicate[]{}));
            }
            return builder.or(predicates.toArray(new Predicate[]{}));
        };
    }

    protected Specification<T> like(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            String value = gc.getValues().stream().findFirst().orElse("").toString();
            value = value.contains("%") ? value : MatchMode.ANYWHERE.toMatchString(value);
            return builder.like(root.get(gc.getField()), value);
        };
    }

    protected Specification<T> in(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            CriteriaBuilder.In<Object> in = builder.in(root.get(gc.getField()));
            gc.getValues().forEach(in::value);
            return in;
        };
    }

    protected Specification<T> notIn(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            CriteriaBuilder.In<Object> in = builder.in(root.get(gc.getField()));
            gc.getValues().forEach(in::value);
            return builder.not(in);
        };
    }

    protected Specification<T> isNull(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.isNull(root.get(gc.getField()));
        };
    }

    protected Specification<T> isNotNull(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.isNotNull(root.get(gc.getField()));
        };
    }

    protected Specification<T> equal(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.equal(root.get(gc.getField()), gc.getValues().toArray()[0]);
        };
    }

    protected Specification<T> notEqual(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.notEqual(root.get(gc.getField()), gc.getValues().toArray()[0]);
        };
    }


    protected Specification<T> between(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.between(root.get(gc.getField()), asComparable(gc.getValues().get(0)), asComparable(gc.getValues().get(1)));
        };
    }

    protected Specification<T> greaterThan(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.greaterThan(root.get(gc.getField()), asComparable(gc.getValues().get(0)));
        };
    }

    protected Specification<T> greaterThanOrEqualTo(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.greaterThanOrEqualTo(root.get(gc.getField()), asComparable(gc.getValues().get(0)));
        };
    }

    protected Specification<T> lessThan(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.lessThan(root.get(gc.getField()), asComparable(gc.getValues().get(0)));
        };
    }

    protected Specification<T> lessThanOrEqualTo(GeneralCondition gc) {
        return (Root<T> root, CriteriaQuery<?> query, CriteriaBuilder builder) -> {
            return builder.lessThanOrEqualTo(root.get(gc.getField()), asComparable(gc.getValues().get(0)));
        };
    }

    private Comparable asComparable(Object value) {
        return (Comparable) value;
    }


}
